<?php
// $Id: Folder.php 27 2010-11-16 12:49:54Z mjohnson60@gmail.com $


define('FOLDER_TYPE_ROOT', 0);

// ie. libraries folder
define('FOLDER_TYPE_LIBRARY', 1);

// ie. libraries/config folder
define('FOLDER_TYPE_SUBLIBRARY', 2);

// ie. sites folder 
define('FOLDER_TYPE_SITES', 3);

// ie. sites/all folder
define('FOLDER_TYPE_SITE', 4);

// ie. themes folder
define('FOLDER_TYPE_THEMES', 5);

// ie. themes/garland folder
define('FOLDER_TYPE_THEME', 6);

// ie. themes/garland/color folder
define('FOLDER_TYPE_SUBTHEME', 7);

// ie. modules folder
define('FOLDER_TYPE_MODULES', 8);

// ie. modules/system folder
define('FOLDER_TYPE_MODULE', 9);

// ie. modules/system/templates folder
define('FOLDER_TYPE_SUBMODULE', 10);

// ie. tests folder
define('FOLDER_TYPE_OTHER', 11);

/**
 * Enter description here ...
 * @author markjohnson
 *
 */
class Folder extends Object
{
	/**
	 * Enter description here ...
	 * @var FOLDER_TYPE
	 */
	public $type = NULL;
	
	/**
	 * Enter description here ...
	 * @var unknown_type
	 */
	public $path = NULL;
	
	/**
	 * Enter description here ...
	 * @var unknown_type
	 */
	public $sort = FALSE;
	
	/**
	 * Enter description here ...
	 * @var unknown_type
	 */
	public $mode = 0755;
	
	/**
	 * Enter description here ...
	 * @var Folder
	 */
	private $parentDir = NULL;
	
	/**
	 * Enter description here ...
	 * @var unknown_type
	 */
	private $messages = array();
	
	/**
	 * Enter description here ...
	 * @var unknown_type
	 */
	private $errors = array();
	
	/**
	 * Holds array of complete directory paths.
	 *
	 * @var array
	 * @access private
	 */
	private $directories = array();
	
	/**
	 * Holds array of complete file paths.
	 *
	 * @var array
	 * @access private
	 */
	private $files = array();
	private $fileNames = array();

	/**
	 * Enter description here ...
	 * @param string $path
	 * @param boolean $create
	 * @param unknown_type $mode
	 */
	public function __construct($path = '', $parent = NULL, $create = FALSE, $mode = NULL)
	{
		if (empty($path))
		{
			if (!defined('ROOT'))
			{
				define('ROOT', dirname(dirname(__FILE__)));
			}
			
			$path = ROOT;
		}
		
		if (!empty($mode))
		{
			$this->mode = $mode;
		}
		
		if (!empty($parent) && is_a($parent, 'Folder'))
		{
			$this->parentDir = $parent;
		}
		
		if (!file_exists($path) && $create == TRUE)
		{
			$this->create($path, $this->mode);
		}
		
		if (!Folder::isAbsolute($path))
		{
			if (!empty($this->parentDir) && is_dir($this->parentDir->pwd() . '/' . $path))
			{
				$path = realpath($this->parentDir->pwd() . '/' . $path);
			}
			else
			{
				$path = realpath($path);
			}
		}
		
		if (!empty($path))
		{
			$this->cd($path);
		}
		
		$this->detectFolderType(TRUE);
	}

	/**
	 * @return the $parentDir
	 */
	public function getParentDir()
	{
		return $this->parentDir;
	}

	/**
	 * @param Folder $parentDir
	 */
	public function setParentDir($parentDir)
	{
		if (empty($this->parentDir) && is_a($parentDir, 'Folder'))
		{
			$this->parentDir = $parentDir;
			return TRUE;
		}
		else
		{
			return FALSE;
		}
	}

	/**
	 * Enter description here ...
	 * @param boolean $refresh
	 * @return FOLDER_TYPE
	 */
	public function detectFolderType($refresh = FALSE)
	{
		if (!isset($this->path) || !Folder::isAbsolute($this->path))
		{
			return FALSE;
		}
		
		if ($this->type !== NULL && !$refresh)
		{
			return $this->type;
		}
		
		$basePath = realpath(ROOT);
		$path = $this->pwd();
		
		if ($basePath !== FALSE)
		{
			if (preg_match('/^' . preg_quote($basePath, '/') . '.*$/i', $path))
			{
				$path = trim(str_replace($basePath, '', $path), '/');
				
				$this->type = FOLDER_TYPE_ROOT;
				
				if (!empty($path))
				{
					$dirs = explode('/', $path);
					
					$this->type = FOLDER_TYPE_ROOT;
					foreach ($dirs as $dir)
					{
						if (empty($dir) || $dir === '.' || $dir === '..' || $dir === '/')
						{
							continue;
						}
						
						$this->detectFolderTypeFromParentDir($dir);
					}
				}
			}
			else
			{
				$this->type = FOLDER_TYPE_OTHER;
			}
		}
		
		return $this->type;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param unknown_type $dir
	 */
	private function detectFolderTypeFromParentDir($dir)
	{
		switch ($this->type)
		{
			case FOLDER_TYPE_ROOT:
				if (strcasecmp($dir, 'libraries') == 0)
				{
					$this->type = FOLDER_TYPE_LIBRARY;
				}
				else if (strcasecmp($dir, 'modules') == 0)
				{
					$this->type = FOLDER_TYPE_MODULES;
				}
				else if (strcasecmp($dir, 'themes') == 0)
				{
					$this->type = FOLDER_TYPE_THEMES;
				}
				else if (strcasecmp($dir, 'sites') == 0)
				{
					$this->type = FOLDER_TYPE_SITES;
				}
				else
				{
					$this->type = FOLDER_TYPE_OTHER;
				}
				
				break;
			case FOLDER_TYPE_LIBRARY:
				$this->type = FOLDER_TYPE_SUBLIBRARY;
				break;
			case FOLDER_TYPE_MODULES:
				$this->type = FOLDER_TYPE_MODULE;
				break;
			case FOLDER_TYPE_MODULE:
				$this->type = FOLDER_TYPE_SUBMODULE;
				break;
			case FOLDER_TYPE_THEMES:
				$this->type = FOLDER_TYPE_THEME;
				break;
			case FOLDER_TYPE_THEME:
				$this->type = FOLDER_TYPE_SUBTHEME;
				break;
			case FOLDER_TYPE_SITES:
				$this->type = FOLDER_TYPE_SITE;
				break;
			case FOLDER_TYPE_SITE:
				if (strcasecmp($dir, 'themes') == 0)
				{
					$this->type = FOLDER_TYPE_THEMES;
				}
				else if (strcasecmp($dir, 'modules') == 0)
				{
					$this->type = FOLDER_TYPE_MODULES;
				}
				else
				{
					$this->type = FOLDER_TYPE_OTHER;
				}
				
				break;
			default:
				break;
		}
	}

	/**
	 * Enter description here ...
	 * 
	 * @param unknown_type $path
	 * @param unknown_type $refresh
	 * @return Ambiguous|boolean
	 */
	public function subdirectory($path, $refresh = FALSE)
	{
		if (empty($this->directories) || $refresh)
		{
			$this->buildDir();
		}
		
		$path = rtrim($path, '/\\');
		
		foreach ($this->directories as $dir)
		{
			if (preg_match('/^' . preg_quote($dir->pwd(), '/i') . '.*/', $path))
			{
				if (($subdir = $dir->subdirectory($path, $refresh)) !== FALSE)
				{
					return $subdir;
				}
			}
		}
		
		return FALSE;
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $path
	 * @param unknown_type $mode
	 * @return boolean
	 */
	public function create($path, $mode = FALSE)
	{
		if (is_dir($path) || empty($path))
		{
			return TRUE;
		}
		
		if (!$mode)
		{
			$mode = $this->mode;
		}
		
		if (is_file($path))
		{
			return FALSE;
		}
		
		$path = rtrim($path, DS);
		$next_path = substr($path, 0, strrpos($path, DS));
		
		if ($this->create($next_path, $mode))
		{
			if (!file_exists($path))
			{
				$old = umask(0);
				if (mkdir($path, $mode))
				{
					umask($old);
					$this->messages[] = sprintf('%s created', $path);
					return TRUE;
				}
				else
				{
					umask($old);
					$this->errors[] = sprintf('%s NOT created', $path);
					return FALSE;
				}
			}
		}
		
		return FALSE;
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $path
	 * @return string|boolean
	 */
	public function cd($path)
	{
		$path = realpath($path);
		if (is_dir($path))
		{
			return $this->path = $path;
		}
		
		return FALSE;
	}

	/**
	 * Enter description here ...
	 * @return NULL
	 */
	public function pwd()
	{
		return $this->path;
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $sort
	 * @param unknown_type $exceptions
	 * @param unknown_type $fullPath
	 * @return multitype:multitype: |multitype:Ambigous <multitype:, unknown> Ambigous <multitype:, string> 
	 */
	public function read($sort = TRUE, $exceptions = FALSE, $fullPath = FALSE)
	{
		$dirs = array();
		$files = array();
		
		if (!$this->pwd())
		{
			return array(
				$dirs, 
				$files
			);
		}
		
		if (is_array($exceptions))
		{
			$exceptions = array_flip($exceptions);
		}
		
		$skipHidden = isset($exceptions['.']) || $exceptions === TRUE;
		
		$dir = opendir($this->path);
		if ($dir === FALSE)
		{
			return array(
				$dirs, 
				$files
			);
		}
		
		while (($item = readdir($dir)) !== FALSE)
		{
			if ($item === '.' || $item === '..' || ($skipHidden && $item[0] === '.') || isset($exceptions[$item]))
			{
				continue;
			}
			
			$path = Folder::addPathElement($this->path, $item);
			if (is_dir($path))
			{
				$dirs[] = $fullPath ? $path : $item;
			}
			else
			{
				$files[] = $fullPath ? $path : $item;
			}
		}
		
		if ($sort || $this->sort)
		{
			sort($dirs);
			sort($files);
		}
		
		closedir($dir);
		
		return array(
			$dirs, 
			$files
		);
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $sort
	 * @param unknown_type $exceptions
	 * @return multitype:multitype: 
	 */
	public function buildDir($sort = TRUE, $exceptions = FALSE, $refresh = FALSE)
	{
		if ((!empty($this->directories) || !empty($this->files)) && !$refresh)
		{
			return;
		}
		
		if (!$this->pwd())
		{
			return;
		}
		
		$this->_buildDir($sort, $exceptions, $refresh);
	}

	/**
	 * Enter description here ...
	 * 
	 * @param unknown_type $sort
	 * @param unknown_type $exceptions
	 * @param unknown_type $refresh
	 */
	private function _buildDir($sort = FALSE, $descend = FALSE, $exceptions = FALSE, $refresh = FALSE)
	{
		if (is_array($exceptions))
		{
			$exceptions = array_flip($exceptions);
		}
		
		$skipHidden = isset($exceptions['.']) || $exceptions === TRUE;
		
		$dir = opendir($this->path);
		if ($dir === FALSE)
		{
			return;
		}
		
		$this->directories = array();
		$this->files = array();
		
		while (($item = readdir($dir)) !== FALSE)
		{
			if ($item[0] === '.' || $item === '..' || ($skipHidden && $item[0] === '.') || isset($exceptions[$item]))
			{
				continue;
			}
			
			if (is_file($this->path . '/' . $item))
			{
				$file = new File($this->path . '/' . $item);
				$file->setFolder($this);
				$this->files[] = $file;
			}
			else if (is_dir($this->path . '/' . $item))
			{
				$folder = new Folder($item, $this);
				
				if ($descend === TRUE)
				{
					$folder->buildDir($sort, $exceptions);
				}
				
				$this->directories[] = $folder;
			}
		}
	}

	/**
	 * Enter description here ...
	 * 
	 * @param string $key
	 */
	private static function filterHiddenFiles($key)
	{
		return !isset($key[0]) || $key[0] != '.';
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $pattern
	 * @param unknown_type $sort
	 * @return multitype:
	 */
	public function find($pattern = '.*', $sort = FALSE)
	{
		if (empty($this->fileNames))
		{
			list($dirs, $files) = $this->read($sort);
			$this->fileNames = $files;
		}
		
		return array_values(preg_grep('/^' . $pattern . '$/i', $this->fileNames));
	}

	/**
	 * Enter description here ...
	 * @param unknown_type $pattern
	 * @param unknown_type $sort
	 * @return multitype:
	 */
	public function findRecursive($pattern = '.*', $directory = FALSE, $sort = FALSE)
	{
		if (empty($this->files) || empty($this->directories))
		{
			$this->buildDir($sort);
		}
		
		$output = array();
		$scan = scandir($this->pwd());
		$output = array_map(array(
			$this, 
			'appendDirectoryStringToFilename'
		), preg_grep('/^' . $pattern . '$/i', $scan));
		
		if (count($output) > 0)
		{
			return $output;
		}
		
		foreach ($this->directories as $dir)
		{
			if (count($output) > 0)
			{
				break;
			}
			
			$output = array_merge($output, $dir->findRecursive($pattern, $sort));
		}
		
		return $output;
	}

	/**
	 * Returns $path with $element added, with correct slash in-between.
	 *
	 * @param string $path Path
	 * @param string $element Element to and at end of path
	 * @return string Combined path
	 * @access public
	 * @static
	 */
	public static function addPathElement($path, $element)
	{
		return rtrim($path, DS) . DS . $element;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param string $filename
	 * @return string
	 */
	public function appendDirectoryStringToFilename($filename)
	{
		return $this->pwd() . '/' . $filename;
	}

	/**
	 * Returns true if given $path is an absolute path.
	 *
	 * @param string $path Path to check
	 * @return bool true if path is absolute.
	 * @access public
	 * @static
	 */
	public static function isAbsolute($path)
	{
		return !empty($path) && ($path[0] === '/' || preg_match('/^[A-Z]:\\\\/i', $path));
	}
}